Skip to content

feat: add the meshStack CLI and the API client it shares with the Terraform provider - #2

Draft
grubmeshi wants to merge 212 commits into
feature/cli-satellitefrom
feature/scaffold-cli
Draft

feat: add the meshStack CLI and the API client it shares with the Terraform provider#2
grubmeshi wants to merge 212 commits into
feature/cli-satellitefrom
feature/scaffold-cli

Conversation

@grubmeshi

@grubmeshi grubmeshi commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Imports client/ from the Terraform provider as a git subtree, split at the provider's v0.25.3, so both tools share one client instead of each carrying its own. The CLI itself follows on top.

Stacked on #3. Paired by branch name with meshcloud/meshfed-release#10866, whose credentials the acceptance suite bootstraps from, and with meshcloud/terraform-provider-meshstack#299, which consumes this.

ClickUp: 86cb61rzz, milestone 86cb61we4.

henryde and others added 30 commits June 20, 2024 13:49
meshStack enforces the Accept header soon, so we have to make sure to always provide it
includes adaptations from PR remarks
nroi and others added 18 commits August 19, 2026 11:19
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e_flags

Replaces the is_four_eyes_enabled bool with a more general enabled_feature_flags
set-of-strings attribute (currently only four_eyes_role_approval), per PR review.
…grations

New spec.config.entraid.idp_alias adopts an identity provider that already
exists in a meshStack instance, instead of always creating one. Optional and
Computed, so an existing integration plans no change.

The alias is immutable. meshStack deletes the identity provider when the
integration is deleted, so expressing a change as a destroy and recreate would
delete a provider the customer may have configured themselves. A plan modifier
rejects the change instead, as version_spec does in
building_block_definition_resource.go, and rejects a change it cannot verify
when state carries no alias to compare against. The framework ships nothing
equivalent: stringplanmodifier has only RequiresReplace variants and
UseStateForUnknown, and a validator cannot see prior state.
meshStack derives the redirect URL and returns it inside spec, which
configuration writes. A computed value there is unreachable under provider
mocks and cannot be supplied by override_* either, so any module wiring that
reads it is untestable (#272). It now lives at status.entraid.redirect_url,
a fully computed container, nested per integration type the way
status.workload_identity_federation nests per cloud.

That also retires a trap: the attribute was Optional as well as Computed, but
meshStack ignores a supplied value, so a configuration that set it failed the
apply with "Provider produced inconsistent result after apply" — after the
integration had been created.

Derived through a local model struct rather than a json:"-" field on the client
type, per the computed-only output field pattern in the resource-development
skill. integrationStatus is shared by the resource and the data source.
A building block definition can now carry a display_name_template, so meshStack
names each ordered building block after the values it was ordered with while the
definition keeps a clean display_name.

Without the attribute a building block is named after display_name, and an empty
string means the same thing. meshStack has to serve the field: an older one drops
it from its response, so an apply that sets it fails Terraform's consistency check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The meshBuildingBlockDefinition meshObject API now carries the meshPanel
Policies tab in spec.approvalPolicies and spec.schedule. They default to no
approval gate and no schedule, which is what meshStack stores for a new
definition.

meshStack validates both against the implementation type of the
definition's latest version, and it rejects a version implementation-type
change while the stored policies are incompatible with the new type. So a
policy the current type cannot honour has to be written after version_spec,
and one the new type cannot honour has to be gone before it. Neutral
policies satisfy every implementation type, so Create writes the definition
neutral, writes the version, then writes the planned policies, and Update
passes through neutral in between when the implementation type changes.
That makes a combined implementation-type and policy change apply in one
step.

Two object validators repeat meshStack's rules at plan time, so an
unsupported combination fails before anything is written. The mock client
learns the same rules, so a unit-test run catches an ordering regression
without a live backend.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
meshStack lets a Building Block Definition input name a tag instead of asking
users to re-type metadata meshStack already governs. Declaring one takes the new
TAG assignment type, a CODE input type and an argument holding the
`<target>.<tagKey>` reference, so the only code change is the enum entry that the
assignment_type validator builds its allowed set from.

The rules that come with it - which targets a definition may read, and why the
input type is fixed - are documented on the two attributes that carry them, and
the terraform example shows the argument referencing a meshstack_tag_definition
rather than a literal key, which also gets the destroy order right.

The meshTagDefinition delete now refuses while a building block reads the tag, so
the tag definition resource says so too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An operator can now mark a definition input optional via version_spec.inputs.*.is_optional, so
whoever fills the Building Block in may leave it blank and the implementation falls back to the
default declared in its own code — a Terraform variable's default, a workflow input's default.
Mirrors meshcloud/meshfed-release#10701.

isOptional is sent with omitempty. false is the backend's default for an absent field, so a
non-optional input serialises exactly as it did before the field existed: every already-stored
content hash stays byte-identical (no currentHashVersion bump, so no released Building Block
re-runs), and a backend that does not know the field still sees the same payload for configs that
do not use the feature. A new fixture pins that equality so a later change cannot silently break it.

MinMeshStackVersion deliberately stays put. Unlike a field the provider always sends, this one only
reaches the backend for someone who opted in, so gating every user of the provider would cost more
than it protects.

ValidateConfig re-imposes the backend's four rules at plan time rather than letting them surface as
a 400 during apply: no optional input on a MANUAL implementation (a person carries the block out, so
there is no code to fall back to), only for the assignment types a person supplies, never for
BOOLEAN (an unset boolean is indistinguishable from false where it is consumed), and never together
with a meshStack default value. Checks whose value is unknown at plan are skipped, keeping the
backend the authority.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two halves belong in one commit because the linter refuses to run when they
are split: a golangci-lint built by go 1.26 rejects a module targeting 1.27 with
"the Go language version (go1.26) used to build golangci-lint is lower than the
targeted Go version (1.27)".

Lint therefore runs differently now. golangci-lint's formatters use the
go/format compiled into the binary, so the formatting they enforce comes from
the Go release that BUILT the linter rather than the toolchain on PATH. Its
version was pinned in two places that could drift - flake.nix took whatever
nixpkgs packaged, CI asked for latest - and neither was tied to go.mod's Go. It
joins tfplugindocs and gotestsum in the tool block instead, so go.mod and go.sum
are the single pin and `task lint` runs `go tool golangci-lint run`, which
builds it with the same Go the code is written against. That also works outside
`nix develop`, which it did not before, so flake.nix no longer carries the
package - one pin fewer to keep in step.

CI keeps golangci-lint-action, because it annotates the pull request diff and a
bare `run:` does not. It installs nothing though: the step before it runs
`go install` for the pinned package and the action uses install-mode none, so
the version it runs is the one in go.mod. The job also moves off
`go-version: stable` onto go.mod's Go, for the same reason.

Rebuilding the linter with go 1.27 changed one alignment group in
client/internal/retry_test.go, which is the behaviour described above showing
up in practice rather than an unrelated edit.

The changelog entry opens a new v0.25.3 section. v0.25.2 is already tagged, so
per the changelog-management skill a pending entry needs a new top section
rather than an amendment to a released one.

Two knock-on changes:

- flake.lock had to move forward, because the June nixpkgs it pinned carries no
  go_1_27 attribute and `nix develop` failed outright with "undefined variable
  'go_1_27'".
- testify goes 1.11.1 -> 1.12.1, raised by MVS because golangci-lint requires
  at least that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A definition input can now declare type JSON_SCHEMA together with the schema its value follows,
matching the new backend input type. meshPanel renders a form from the schema, and the value
reaches the building block as JSON text, exactly like a CODE input.

JSON_SCHEMA is deliberately not an entry of MeshBuildingBlockIOTypes: that enum describes the
type of a value, and a building block's own inputs keep reporting CODE for these. Only the
definition input schema offers it, via MeshBuildingBlockDefinitionInputTypes.

ValidateConfig re-imposes the pairing the framework cannot express — json_schema is required for
the type and rejected for every other — so a mismatch fails at plan time rather than as a 400.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The list of types a definition input may declare is the full IO type set plus
JSON_SCHEMA, so spell it that way instead of repeating all nine entries. A new
Enum.With copies via slices.Concat rather than appending, so the package-level
MeshBuildingBlockIOTypes can never be written into through its shared backing
array.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wording led with the value the input produces, which reads as "CODE with a
schema attached" and hides what the type actually does: it gives the input a
form of its own, declared by json_schema, that meshPanel renders in place of a
single field. Lead with the form everywhere the type is described — the `type`
attribute, `json_schema`, the ValidateConfig messages, the example and the
changelog — and keep the CODE equivalence as the follow-up it is, since that is
only true of what the form produces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows the backend rename: the type names the value the input holds, and the schema
stays a separate argument on it. The type ships first in meshStack 2026.37.0, which the
changelog entry now states.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The client was the Terraform provider's own package. The CLI needs the same calls, and a
public repository is the only place both can depend on it, so the client's home is here now
and the provider consumes it as a module.

This is a merge rather than a copy so that the client keeps the history it has in the
provider: `git log` and `git blame` on these files still reach every change made there, and
`git subtree` can still move a change either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

git-subtree-dir: client
git-subtree-mainline: f2ba137
git-subtree-split: cf78ea1
@grubmeshi
grubmeshi force-pushed the feature/scaffold-cli branch 2 times, most recently from d2fe4b5 to 843633e Compare September 9, 2026 18:44
grubmeshi and others added 2 commits September 9, 2026 21:21
The client's imports name this module now, and the HTTP machinery it was built on sits at the
module root as internal/http rather than under client/internal: pkg/oidc and pkg/auth need the
same client, and Go's internal rule closes client/internal to both.

internal/http is tested from the outside, through the types its callers parse answers into, so
its test reaches for pkg/oidc/jwt and pulls pkg/meshstack and pkg/oidc/scope along. Those three
land here instead of with the rest of the CLI so that this commit's own suite runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
meshstack authenticates against meshStack — a browser login, an API key, an API token, or the
local dev stack's own published credentials — keeps the result in a named profile, and lists the
workspaces that credential reaches. Every setting is declared once beside the domain it belongs
to, so the CLI's flags and the Terraform provider's block resolve the same thing the same way.

The plumbing that ships it comes along: goreleaser builds the archives, a container image goes to
GHCR, and the dependency policy that keeps the provider's dependency tree small sits in the linter
configuration, where widening it is a deliberate edit rather than a lint fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@grubmeshi
grubmeshi force-pushed the feature/scaffold-cli branch from 843633e to 394e45e Compare September 9, 2026 19:22
@grubmeshi

Copy link
Copy Markdown
Collaborator Author

Superseded by the stack #3 (the satellite hull) and #4 (the client subtree and the CLI), which rebuild this branch on top of a registered satellite.

Closing it also matters for CI: meshfed-release's satellite-resolve pairs a satellite by branch name, and two open pull requests with the same head branch make it fall back to the branch tip instead of the merge commit.

@grubmeshi grubmeshi closed this Sep 9, 2026
@grubmeshi grubmeshi reopened this Sep 9, 2026
@grubmeshi grubmeshi changed the title feat: bootstrap the meshStack CLI and move the API client in feat: add the meshStack CLI and the API client it shares with the Terraform provider Sep 9, 2026
@grubmeshi
grubmeshi changed the base branch from main to feature/cli-satellite September 9, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.